package com.zenika.zentracker.spring;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.hibernate.cfg.Environment;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.HibernateTransactionManager;
import org.springframework.orm.hibernate3.LocalSessionFactoryBean;

import com.zenika.zentracker.hibernate.interceptor.SessionStatisticsInterceptor;
import com.zenika.zentracker.hibernate.jdbc.ZenTrackerBatchingFactory;
import com.zenika.zentracker.hibernate.listener.EntityCheckDirtyListener;

public class ZenTrackerConfigurer implements BeanFactoryPostProcessor, PriorityOrdered {

    private static final String CHECK_DIRTY_LISTENER_BEAN_NAME = "checkDirtyListener";

    private static final String TX_MANAGER_STATISTICS_INTERCEPTOR = "txManagerStatisticsInterceptor";

    private static final String HIB_TEMPLATE_STATISTICS_INTERCEPTOR = "hibTemplateStatisticsInterceptor";
    
    private static final String MBEAN_EXPORTER_BEAN_NAME = "jmxSimpleExporter";
    
    public static final String MBEAN_EXPORTER_BEAN_ALIAS = "com.zenika.zentracker.MBeanExporter.alias";
    
    public static final String BOOLEAN_EXPORT_JMX_BEAN = "com.zenika.exportJMXStatistics";

    private static final String SESSION_FACTRY_ENTITY_INTERCEPTOR = "entityInterceptor";

    private static final String ENTITY_INTERCEPTOR = "entityInterceptor";
    
    private static final String ENTITY_INTERCEPTOR_BEAN_NAME = "entityInterceptorBeanName";

    private static final String HIBERNATE_PROPERTIES = "hibernateProperties";

    private static final String EVENT_LISTENERS = "eventListeners";

    private static final String FLUSH_ENTITY_EVENT = "flush-entity";
    
    private static final String INTERCEPTOR_SHOULD_NOT_BE_SINGLETON = " is using an Interceptor of type Singleton. It is incompatible with the ZenTracker Interceptor that should be a new instance for every Hibernate Session";  
    
    private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered

    /** Tells if statistics should be enabled via JMX */
    private boolean exportJMXStatistics;
 
    /** Bean name for the checkDirtyListener bean */
    private String checkDirtyListener = CHECK_DIRTY_LISTENER_BEAN_NAME;

    /** Bean name for the statisticsInterceptor bean */
    private String hibTemplateStatisticsInterceptor = HIB_TEMPLATE_STATISTICS_INTERCEPTOR;
    private String txManagerStatisticsInterceptor = TX_MANAGER_STATISTICS_INTERCEPTOR;
    
    /** Bean name for the jmxSimpleExporter bean */
    private String jmxSimpleExporter = MBEAN_EXPORTER_BEAN_NAME;
    
    /* (non-Javadoc)
     * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
     */
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactoryToProcess) throws BeansException {
        addSingletonBeanDefinition(beanFactoryToProcess, checkDirtyListener, EntityCheckDirtyListener.class);
        BeanDefinition zenTrackerTxManagerInterceptor = addPrototypeBeanDefinition(beanFactoryToProcess, txManagerStatisticsInterceptor, SessionStatisticsInterceptor.class);
        BeanDefinition zenTrackerHibTemplateInterceptor = addPrototypeBeanDefinition(beanFactoryToProcess, hibTemplateStatisticsInterceptor, SessionStatisticsInterceptor.class);
        
        String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
        boolean isMissingMBeanExporterDefinition = true;
        
        for (int i = 0; i < beanNames.length; i++) {
            String beanName = beanNames[i];
            BeanDefinition beanDefinition = beanFactoryToProcess.getBeanDefinition(beanName);

            if (isBeanFromExpectedName(beanDefinition, LocalSessionFactoryBean.class)) {
                enhanceSessionFactoryBeanDefinition(beanDefinition);
            }
            
            if (isBeanFromExpectedName(beanDefinition, HibernateTransactionManager.class)) {
                enhanceHibernateTransactionManagerBeanDefinition(beanDefinition, zenTrackerTxManagerInterceptor);
            }

            if (isBeanFromExpectedName(beanDefinition, HibernateTemplate.class)) {
                enhanceHibernateTemplateBeanDefinition(beanDefinition, zenTrackerHibTemplateInterceptor);
            }
            
            if (isBeanFromExpectedName(beanDefinition, MBeanExporter.class)) {
                beanFactoryToProcess.registerAlias(beanName, MBEAN_EXPORTER_BEAN_ALIAS);
                isMissingMBeanExporterDefinition = false;
            }
        }
        
        BeanDefinition exportJMXStBeanDefinition = addSingletonBeanDefinition(beanFactoryToProcess, BOOLEAN_EXPORT_JMX_BEAN, Boolean.class);
        exportJMXStBeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, new Boolean(exportJMXStatistics));
      
        if (isMissingMBeanExporterDefinition && exportJMXStatistics) {
            beanFactoryToProcess.registerAlias(jmxSimpleExporter, MBEAN_EXPORTER_BEAN_ALIAS);
            addSingletonBeanDefinition(beanFactoryToProcess, jmxSimpleExporter, MBeanExporter.class);
        }
    }

    /**
     * @param beanFactoryToProcess
     * @param beanNameForRegistration
     * @param beanClass
     * @return
     */
    private BeanDefinition addSingletonBeanDefinition(final ConfigurableListableBeanFactory beanFactoryToProcess, final String beanNameForRegistration, final Class beanClass) {
        return this.addBeanDefinition(beanFactoryToProcess, beanNameForRegistration, beanClass, BeanDefinition.SCOPE_SINGLETON);
    }

    /**
     * @param beanFactoryToProcess
     * @param beanNameForRegistration
     * @param beanClass
     * @return
     */
    private BeanDefinition addPrototypeBeanDefinition(final ConfigurableListableBeanFactory beanFactoryToProcess, final String beanNameForRegistration, final Class beanClass) {
        return this.addBeanDefinition(beanFactoryToProcess, beanNameForRegistration, beanClass, BeanDefinition.SCOPE_PROTOTYPE);
    }

    /**
     * @param beanFactoryToProcess
     * @param beanNameForRegistration
     * @param beanClass
     * @param scope
     * @return
     */
    private BeanDefinition addBeanDefinition(final ConfigurableListableBeanFactory beanFactoryToProcess, final String beanNameForRegistration, final Class beanClass, final String scope) {
        if (beanFactoryToProcess.containsBeanDefinition(beanNameForRegistration)) {
            throw new BeanCreationException("The bean with name " + beanNameForRegistration + " already exists. Please choose another name by using the property [" + beanNameForRegistration + "]");
        }
        
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(beanClass);
        beanDefinition.setScope(scope);
        
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactoryToProcess;
        
        defaultListableBeanFactory.registerBeanDefinition(beanNameForRegistration, beanDefinition);
        
        return beanDefinition;
    }

    /**
     * 
     * @param beanName
     * @param beanDefinition
     * @return
     */
    private boolean isBeanFromExpectedName(BeanDefinition beanDefinition, Class type) {
        if (beanDefinition.getBeanClassName() == null) {
            // Case where the bean has no class. Typically relying on a parent bean to get the class. 
            return false;
        }

        return beanDefinition.getBeanClassName().equals(type.getName());
    }

    /**
     * @param beanDefinition
     */
    private void enhanceSessionFactoryBeanDefinition(BeanDefinition beanDefinition) {
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();

        // Check if an Interceptor is already defined. If yes, raise an exception
        PropertyValue value = propertyValues.getPropertyValue(SESSION_FACTRY_ENTITY_INTERCEPTOR);
        if (value != null) 
            throw new FatalBeanException("The SessionFactory " + INTERCEPTOR_SHOULD_NOT_BE_SINGLETON);
        
        // Modify the LocalSessionFacotryBean class name
        beanDefinition.setBeanClassName(ZenTrackerLocalSessionFactoryBean.class.getName());
        
        addJdbcFactoryClassProperty(propertyValues.getPropertyValue(HIBERNATE_PROPERTIES));

        addZenTrackerEventListener(propertyValues);

        modifySessionContextClass(propertyValues.getPropertyValue(HIBERNATE_PROPERTIES));
    }

    /**
     * @param beanDefinition
     */
    private void enhanceHibernateTransactionManagerBeanDefinition(BeanDefinition beanDefinition, BeanDefinition zenTrackerInterceptor) {
        // Modify the LocalSessionFacotryBean class name
        beanDefinition.setBeanClassName(ZenTrackerHibernateTransactionManager.class.getName());

        MutablePropertyValues txManagerPropertyValues = beanDefinition.getPropertyValues();

        String exceptionMessage = "The TransactionManager " + INTERCEPTOR_SHOULD_NOT_BE_SINGLETON;
        
        if (txManagerPropertyValues.contains(ENTITY_INTERCEPTOR)) {
            throw new FatalBeanException(exceptionMessage);
        }

        PropertyValue propertyValue = txManagerPropertyValues.getPropertyValue(ENTITY_INTERCEPTOR_BEAN_NAME);
        if (propertyValue != null) {
            Object value = propertyValue.getValue();
            if (BeanReference.class.isAssignableFrom(value.getClass()))
                throw new FatalBeanException(exceptionMessage);

            // Add on the definition of the ZenTracker interceptor a dependency to the user defined interceptor
            String userDefinedBeanName = ((TypedStringValue) value).getValue();
            zenTrackerInterceptor.getPropertyValues().addPropertyValue(new PropertyValue("userDefinedInterceptor", new RuntimeBeanReference(userDefinedBeanName)));
            
            // Modify the property dependency
            txManagerPropertyValues.getPropertyValueList().remove(propertyValue);
        }

        txManagerPropertyValues.addPropertyValue(new PropertyValue(ENTITY_INTERCEPTOR_BEAN_NAME, new TypedStringValue(txManagerStatisticsInterceptor)));

    }

    /**
     * @param beanDefinition
     */
    private void enhanceHibernateTemplateBeanDefinition(BeanDefinition beanDefinition, BeanDefinition zenTrackerInterceptor) {
        MutablePropertyValues hibTemplatePropertyValues = beanDefinition.getPropertyValues();

        String exceptionMessage = "The HibernateTemplate " + INTERCEPTOR_SHOULD_NOT_BE_SINGLETON;
        
        if (hibTemplatePropertyValues.contains(ENTITY_INTERCEPTOR)) {
            throw new FatalBeanException(exceptionMessage);
        }

        PropertyValue propertyValue = hibTemplatePropertyValues.getPropertyValue(ENTITY_INTERCEPTOR_BEAN_NAME);
        if (propertyValue != null) {
            Object value = propertyValue.getValue();
            if (BeanReference.class.isAssignableFrom(value.getClass()))
                throw new FatalBeanException(exceptionMessage);

            // Add on the definition of the ZenTracker interceptor a dependency to the user defined interceptor
            String userDefinedBeanName = ((TypedStringValue) value).getValue();
            zenTrackerInterceptor.getPropertyValues().addPropertyValue(new PropertyValue("userDefinedInterceptor", new RuntimeBeanReference(userDefinedBeanName)));
            
            // Modify the property dependency
            hibTemplatePropertyValues.getPropertyValueList().remove(propertyValue);
        }
        
        hibTemplatePropertyValues.addPropertyValue(new PropertyValue(ENTITY_INTERCEPTOR_BEAN_NAME, new TypedStringValue(hibTemplateStatisticsInterceptor)));
    }

    /**
     * @param eventListeners
     */
    private void addZenTrackerEventListener(MutablePropertyValues propertyValues) {
        PropertyValue eventListeners = propertyValues.getPropertyValue(EVENT_LISTENERS);

        // Case no Event Listener are registered
        if (eventListeners == null) {
            Map newEventListener = new ManagedMap();
            addListenerForEventType(newEventListener);
            propertyValues.addPropertyValue(new PropertyValue(EVENT_LISTENERS, newEventListener));
        } else {
            // the eventListeners property is set. It needs to be enhanced.
            Object value = eventListeners.getValue();
    
            if (value instanceof Map) {
                Map eventListenersMap = (Map) value;
                boolean flushEntityEventExists = false;
                for (Iterator it = eventListenersMap.entrySet().iterator(); it.hasNext();) {
                    Map.Entry entry = (Map.Entry) it.next();
                    TypedStringValue listenerType = (TypedStringValue) entry.getKey();
                    // Check for the flush-entity event
                    if (listenerType.getValue().equals(FLUSH_ENTITY_EVENT)) {
                        flushEntityEventExists = true;
                        Object listenerObject = entry.getValue();
                        if (listenerObject instanceof List) {
                            List listeners = (List) listenerObject;
                            listeners.add(new RuntimeBeanReference(this.checkDirtyListener));
                        }
                        else {
                            ManagedList managedList = new ManagedList();
                            managedList.add(listenerObject);
                            managedList.add(new RuntimeBeanReference(this.checkDirtyListener));
                            eventListenersMap.put(entry.getKey(), managedList);
                        }
                    }
                }
                
                if (!flushEntityEventExists) {
                    addListenerForEventType(eventListenersMap);                    
                }
            }
        }
    }

    /**
     * @param eventListenersMap
     */
    private void addListenerForEventType(Map eventListenersMap) {
        eventListenersMap.put(
                new TypedStringValue(FLUSH_ENTITY_EVENT), 
                new RuntimeBeanReference(this.checkDirtyListener));
    }

    /**
     * @param hibernateProperties
     */
    private void addJdbcFactoryClassProperty(PropertyValue hibernateProperties) {
        Object value = hibernateProperties.getValue();
        String batchStrategy = Environment.BATCH_STRATEGY;
        
        if (value instanceof Properties) {
            Properties props = (Properties) value;
            
            Enumeration keys = props.keys();
            while (keys.hasMoreElements()) {
                TypedStringValue key = (TypedStringValue) keys.nextElement();
                if (key.getValue().equals(batchStrategy)) {
                    throw new FatalBeanException("The property " + batchStrategy + " is already defined in the SessionFactory bean configuration");
                }
            }
            
            // The property has not been found, it needs to be added.
            props.put(
                new TypedStringValue(batchStrategy), 
                new TypedStringValue(ZenTrackerBatchingFactory.class.getName()));
        }
    }


    /**
     * @param hibernateProperties
     */
    private void modifySessionContextClass(PropertyValue hibernateProperties) {
        Object value = hibernateProperties.getValue();
        String contextClass = Environment.CURRENT_SESSION_CONTEXT_CLASS;
        
        if (value instanceof Properties) {
            Properties props = (Properties) value;
            
            Enumeration keys = props.keys();
            while (keys.hasMoreElements()) {
                TypedStringValue key = (TypedStringValue) keys.nextElement();
                if (key.getValue().equals(contextClass)) {
                    throw new FatalBeanException("The property " + contextClass + " is already defined in the SessionFactory bean configuration");
                }
            }
            
            // The property has not been found, it needs to be added.
            props.put(
                new TypedStringValue(contextClass), 
                new TypedStringValue(ZenTrackerSpringSessionContext.class.getName()));
        }
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public int getOrder() {
        return this.order;
    }

    public void setCheckDirtyListener(String checkDirtyListenerBeanName) {
        this.checkDirtyListener = checkDirtyListenerBeanName;
    }

    public void setHibTemplateStatisticsInterceptor(String hibTemplateStatisticsInterceptor) {
        this.hibTemplateStatisticsInterceptor = hibTemplateStatisticsInterceptor;
    }

    public void setTxManagerStatisticsInterceptor(String txManagerStatisticsInterceptor) {
        this.txManagerStatisticsInterceptor = txManagerStatisticsInterceptor;
    }

    public void setJmxSimpleExporter(String jmxSimpleExporter) {
        this.jmxSimpleExporter = jmxSimpleExporter;
    }

    public void setExportJMXStatistics(boolean exportJMXStatistics) {
        this.exportJMXStatistics = exportJMXStatistics;
    }

}
